Skip to content

feat(ui): put the web app on the documentation site's palette - #194

Merged
vishr merged 2 commits into
mainfrom
theme/align-app-with-site
Aug 24, 2026
Merged

feat(ui): put the web app on the documentation site's palette#194
vishr merged 2 commits into
mainfrom
theme/align-app-with-site

Conversation

@vishr

@vishr vishr commented Aug 24, 2026

Copy link
Copy Markdown
Member

The web app ran on Mantine's stock theme — teal primary, Inter named in the font stack but never loaded, and no dark mode at all. The documentation site has a palette with reasoning behind it (Ayu Dark surfaces, a violet accent, IBM Plex split by role) and a comment claiming the app already shared it. It did not: not a hex, not a typeface, not a variable name crossed between them.

The collision

healthColor() returned "teal" for a healthy service while primaryColor: "teal" painted every button, link and badge. One hue meant both "healthy" and "clickable" on the same screen. The site picked violet precisely to avoid this — violet is the one hue in the palette that carries no status meaning.

What this does

  • ui/tokens.ts — new. The Ayu ramp, the violet accent, the three health hues (#7fd962 / #ffb454 / #f26d78, the site's own), a fourth info hue, chart chrome, and a categorical series palette drawn from the brand mark's ribbons. Values mirror site/src/styles/fanout.css; the two remain separate sources kept in step by hand.
  • ui/theme.ts — binds those to Mantine under semantic names: brand, ok, warn, bad, info. A component asks for the accent or a health state and gets whatever the palette holds, so re-hueing is a one-file change. primaryShade: {light: 7, dark: 5} picks the accent each scheme already uses on the site; autoContrast handles the two-shade fill; Mantine's dark ramp is replaced by Ayu, which lands the app on the documentation's exact ground.
  • Dark mode, for the first time: defaultColorScheme="auto", a header toggle, and the scheme resolved by an inline script in index.html's head so the first paint lands on the right ground. Paired theme-color metas replace the stray #f6f5f1.
  • The typeface ships rather than being named: IBM Plex Sans + Mono, eight weights in the host app, four inlined into each single-file embedded view. assetsInlineLimit had to be raised or the build would have emitted loose .woff2 files it never copies into internal/mcp/apps.
  • Charts take resolved values from the same ramps (chartTheme / statusHex / seriesColor) instead of six hardcoded green-greys and a Mantine hue set that included the health colors.

Bugs this surfaced

All three were invisible while the app was light-only:

  • The sign-in card hardcoded rgba(255, 255, 255, 0.9), so the auth page would have stayed white under a dark app — with no toggle on that screen to escape.
  • Dashboard metric tiles used bg="gray.0", the same color in both schemes; the four health tiles rendered white-on-white.
  • mcp-app-frame.tsx passed theme: "light" as a literal to the app bridge, which would have left every embedded MCP view light inside a dark host.

Verification

just check passes end to end. Exercised against a real binary with 3,746 seeded spans in both schemes, toggling each direction, plus the signed-out auth page in dark.

Two things reviewers should know: the embedded MCP views are type-checked but not visually exercised (chat needs an AI provider key), and running just check on Go 1.27 fails TestAppendJSONString_MatchesEncodingJSON and regenerates THIRD_PARTY_NOTICES with one extra component — go.mod pins 1.26.5 and that is what CI and this branch used.

🤖 Generated with Claude Code

The app ran on Mantine's stock theme: teal primary, Inter named but never
loaded, and no dark mode at all. The documentation site, meanwhile, has a
palette with reasoning behind it — Ayu Dark surfaces, a violet accent, IBM
Plex split by role — and a comment claiming the app already shared it. It
did not. Nothing crossed between them: not a hex, not a typeface, not a
variable name.

The teal was the substantive problem. `healthColor()` returned "teal" for a
healthy service while `primaryColor: "teal"` painted every button and link,
so one hue meant both "healthy" and "clickable" on the same screen. The site
picked violet precisely to avoid that, violet being the one hue in the
palette that carries no status.

ui/tokens.ts now holds the palette — the Ayu ramp, the violet accent, the
three health hues and a categorical series palette taken from the brand
mark — and ui/theme.ts binds it to Mantine under semantic names: brand, ok,
warn, bad, info. A component asks for the accent or for a health state and
gets whatever the palette holds, so re-hueing the product is a change to one
file. The values mirror site/src/styles/fanout.css; the two are still
separate sources kept in step by hand.

Dark mode ships with it: defaultColorScheme="auto", a header toggle, and the
scheme resolved by an inline script in the document head so the first paint
lands on the right ground. The typeface is shipped rather than named — eight
weights in the host, four inlined into each single-file embedded view, which
needed assetsInlineLimit raised or the build would have emitted loose .woff2
files it never copies.

Three latent bugs surfaced, all of them invisible while the app was
light-only: the sign-in card hardcoded a white background, dashboard metric
tiles used bg="gray.0" and rendered white-on-white, and the MCP app bridge
passed theme: "light" as a literal, which would have left every embedded
view light inside a dark host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two browser stages copied `ui/theme.ts` by name, so `ui/tokens.ts` never
reached the container and both `bun run build` invocations failed on an
unresolved import. Globbing the directory means the next shared module does
not have to remember this file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vishr
vishr merged commit ba5a17c into main Aug 24, 2026
8 checks passed
@vishr
vishr deleted the theme/align-app-with-site branch August 24, 2026 18:32
vishr added a commit that referenced this pull request Aug 24, 2026
* feat(ui): put the web app on the documentation site's palette

The app ran on Mantine's stock theme: teal primary, Inter named but never
loaded, and no dark mode at all. The documentation site, meanwhile, has a
palette with reasoning behind it — Ayu Dark surfaces, a violet accent, IBM
Plex split by role — and a comment claiming the app already shared it. It
did not. Nothing crossed between them: not a hex, not a typeface, not a
variable name.

The teal was the substantive problem. `healthColor()` returned "teal" for a
healthy service while `primaryColor: "teal"` painted every button and link,
so one hue meant both "healthy" and "clickable" on the same screen. The site
picked violet precisely to avoid that, violet being the one hue in the
palette that carries no status.

ui/tokens.ts now holds the palette — the Ayu ramp, the violet accent, the
three health hues and a categorical series palette taken from the brand
mark — and ui/theme.ts binds it to Mantine under semantic names: brand, ok,
warn, bad, info. A component asks for the accent or for a health state and
gets whatever the palette holds, so re-hueing the product is a change to one
file. The values mirror site/src/styles/fanout.css; the two are still
separate sources kept in step by hand.

Dark mode ships with it: defaultColorScheme="auto", a header toggle, and the
scheme resolved by an inline script in the document head so the first paint
lands on the right ground. The typeface is shipped rather than named — eight
weights in the host, four inlined into each single-file embedded view, which
needed assetsInlineLimit raised or the build would have emitted loose .woff2
files it never copies.

Three latent bugs surfaced, all of them invisible while the app was
light-only: the sign-in card hardcoded a white background, dashboard metric
tiles used bg="gray.0" and rendered white-on-white, and the MCP app bridge
passed theme: "light" as a literal, which would have left every embedded
view light inside a dark host.


* fix(docker): copy every shared module into the browser build stages

The two browser stages copied `ui/theme.ts` by name, so `ui/tokens.ts` never
reached the container and both `bun run build` invocations failed on an
unresolved import. Globbing the directory means the next shared module does
not have to remember this file.


---------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant